Multi project support for the C# parser#847
Multi project support for the C# parser#847HorvathDaniel15 wants to merge 2 commits intoEricsson:feature/csharp_pluginfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the C# parser to accept richer inputs (solutions/projects) and to better handle multiple inputs, enabling “multi-project” parsing via Roslyn/MSBuild rather than only directory-based discovery.
Changes:
- Add input validation and safer log parsing in the C++ wrapper (avoid empty-input and empty-line crashes; log non file-status output).
- Update the C# parser to support
.sln/.slnxand.csprojinputs usingMSBuildWorkspace, with a fallback path that extracts project paths from solution files. - Add Roslyn MSBuild workspace dependency and ignore
.DS_Store.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
plugins/csharp/parser/src/csharpparser.cpp |
Validates inputs and hardens parsing of the C# parser output stream. |
plugins/csharp/parser/src_csharp/Program.cs |
Implements solution/project loading via MSBuildWorkspace and multi-project processing with parallel document parsing. |
plugins/csharp/parser/src_csharp/CSharpParser.csproj |
Adds Microsoft.CodeAnalysis.Workspaces.MSBuild to support solution/project loading. |
.gitignore |
Ignores macOS .DS_Store. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static async Task<int> Main(string[] args) | ||
| { | ||
| MSBuildLocator.RegisterDefaults(); | ||
| _rootDir = new List<string>(); | ||
| int threadNum = 4; |
There was a problem hiding this comment.
MSBuildLocator.RegisterDefaults() can throw (e.g., when no compatible MSBuild instance is found), which would currently crash the process before you emit any parser-friendly error output. Wrap registration in the existing argument-parsing try/catch (or a dedicated try/catch) and return a non-zero exit code with a clear message when registration fails.
There was a problem hiding this comment.
This thread was resolved, but no fix is present in the codebase.
| string csharpConnectionString = transformConnectionString(); | ||
|
|
||
| var options = new DbContextOptionsBuilder<CsharpDbContext>() | ||
| .UseNpgsql(csharpConnectionString) | ||
| .Options; | ||
|
|
||
| CsharpDbContext _context = new CsharpDbContext(options); | ||
| _context.Database.Migrate(); |
There was a problem hiding this comment.
The database context is created and migrations are applied before validating that any input paths were provided. This can cause unnecessary (and potentially destructive/slow) DB work even when the CLI invocation is invalid; consider validating args/_rootDir first, then migrating/creating the DbContext only for valid runs (and disposing it via using/await using).
There was a problem hiding this comment.
This thread was resolved, but no fix is present in the codebase.
| Console.Error.WriteLine($"Unsupported file type: {primaryInput}"); | ||
| Console.Error.WriteLine("Supported: .sln, .slnx, .csproj"); |
There was a problem hiding this comment.
This code writes important user-facing errors to stderr, but the C++ wrapper currently captures only stdout (bp::std_out > log). As a result, failures/diagnostics may be invisible to the caller. Consider writing these diagnostics to stdout (with a prefix) or ensure stderr is also captured/redirected by the wrapper.
| Console.Error.WriteLine($"Unsupported file type: {primaryInput}"); | |
| Console.Error.WriteLine("Supported: .sln, .slnx, .csproj"); | |
| WriteLine($"ERROR: Unsupported file type: {primaryInput}"); | |
| WriteLine("ERROR: Supported: .sln, .slnx, .csproj"); |
There was a problem hiding this comment.
We can keep it the current way, but this static usage for System.Console is quite odd. Please remove that and instead write Console.WriteLine() on each usage for clarity and convention purposes.
| private static async Task<Solution> LoadInputAsync(string inputPath) | ||
| { | ||
| if (File.Exists(inputPath)) | ||
| { | ||
| if (inputPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || | ||
| inputPath.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return await LoadSolutionAsync(inputPath); | ||
| } | ||
| if (inputPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return await LoadProjectAsync(inputPath); | ||
| } | ||
| else | ||
| { | ||
| Console.Error.WriteLine($"Unsupported file type: {inputPath}"); | ||
| Console.Error.WriteLine("Supported: .sln, .slnx, .csproj"); | ||
| return null; | ||
| } | ||
| } | ||
| if (Directory.Exists(inputPath)) | ||
| { | ||
| return null; | ||
| } | ||
| else | ||
| { | ||
| Console.Error.WriteLine($"Input path not found: {inputPath}"); | ||
| return null; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
LoadInputAsync is not referenced anywhere in the file (no call sites). Keeping this unused method (and its error-printing branches) adds dead code and increases maintenance surface. Either remove it or refactor Main to use it so there is a single, tested input-loading pathway.
| private static async Task<Solution> LoadInputAsync(string inputPath) | |
| { | |
| if (File.Exists(inputPath)) | |
| { | |
| if (inputPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || | |
| inputPath.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return await LoadSolutionAsync(inputPath); | |
| } | |
| if (inputPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return await LoadProjectAsync(inputPath); | |
| } | |
| else | |
| { | |
| Console.Error.WriteLine($"Unsupported file type: {inputPath}"); | |
| Console.Error.WriteLine("Supported: .sln, .slnx, .csproj"); | |
| return null; | |
| } | |
| } | |
| if (Directory.Exists(inputPath)) | |
| { | |
| return null; | |
| } | |
| else | |
| { | |
| Console.Error.WriteLine($"Input path not found: {inputPath}"); | |
| return null; | |
| } | |
| } |
There was a problem hiding this comment.
@HorvathDaniel15 Is this really dead code, or Copilot just did not find its usage?
|
|
||
| foreach (var group in documentGroups) | ||
| { | ||
| tasks.Add(Task.Run(async () => | ||
| { | ||
| var options = new DbContextOptionsBuilder<CsharpDbContext>() | ||
| .UseNpgsql(csharpConnectionString) | ||
| .Options; | ||
| var localDbContext = new CsharpDbContext(options); |
There was a problem hiding this comment.
localDbContext is created inside each worker task but never disposed. Since DbContext holds pooled connections and unmanaged resources, this can leak connections under load. Use (await) using for the context (and consider creating options once outside the loop).
| foreach (var group in documentGroups) | |
| { | |
| tasks.Add(Task.Run(async () => | |
| { | |
| var options = new DbContextOptionsBuilder<CsharpDbContext>() | |
| .UseNpgsql(csharpConnectionString) | |
| .Options; | |
| var localDbContext = new CsharpDbContext(options); | |
| var options = new DbContextOptionsBuilder<CsharpDbContext>() | |
| .UseNpgsql(csharpConnectionString) | |
| .Options; | |
| foreach (var group in documentGroups) | |
| { | |
| tasks.Add(Task.Run(async () => | |
| { | |
| await using var localDbContext = new CsharpDbContext(options); |
There was a problem hiding this comment.
This thread was resolved, but no fix is present in the codebase. Please provide a counter-argument if no code change is required.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Console.WriteLine($"Loading solution: {solutionPath}"); | ||
| var workspace = CreateMsBuildWorkspace(); | ||
|
|
||
| var solution = await workspace.OpenSolutionAsync(solutionPath); |
There was a problem hiding this comment.
The MSBuildWorkspace created here is not disposed. Since loading a solution can allocate significant resources, this can cause avoidable memory/file-handle usage. Wrap the workspace in a using/await using (or otherwise ensure Dispose() is called) once loading is complete.
There was a problem hiding this comment.
Valid memory optimization concern.
| if (projectPaths.Count == 0) | ||
| { | ||
| Console.Error.WriteLine($"No C# projects found in solution: {solutionPath}"); | ||
| return; |
There was a problem hiding this comment.
When no C# projects are found in the solution, this method logs an error and returns, but Main will still exit with code 0. That makes the wrapper/parser treat the run as successful even though nothing was parsed. Consider propagating failure (e.g., return a bool/int result or throw) so Main can exit non-zero in this case.
| return; | |
| throw new InvalidOperationException( | |
| $"No C# projects found in solution: {solutionPath}"); |
There was a problem hiding this comment.
Valid concern, since in case of other input errors, the program return with a non-zero exit code.
Resolves #823